package com.example.mysql_mybatis_demo.springsource;

import com.example.mysql_mybatis_demo.common.utils.R;
import com.example.mysql_mybatis_demo.springsource.config.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.SpringApplication;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RequestMapping("/mysql_mybatis_demo/springsource")
@RestController
public class SpringSourceController {

    // 一个非常重要的点是，如果一个 Bean 拥有多个子 Bean, 那么在自动注入这个Bean时，必须使用 @Qualifier 指定到底要使用哪个Bean的id
    @Qualifier("parent")
    @Autowired
    ParentTestBean parentTestBean;

    @Autowired
    DerivedTestBean derivedTestBean;

    /**
     * 测试Bean的继承关系
     *
     * @return
     */
    @GetMapping("/test1")
    public R test1() {
        System.out.println(parentTestBean.getName() + ":" + parentTestBean.getAge());  // 输出 null:20 , 父Bean的属性不受子Bean的影响，只和初始化父Bean的设置有关
        // System.out.println(parentTestBean.getName() + ":" + parentTestBean.getAddress());  // 报错，parentTestBean不能获得子Bean中独有的Address属性
        System.out.println(derivedTestBean.getName() + ":" + derivedTestBean.getAge());  // 输出 child:20 , 子Bean等值继承父Bean中的属性，如果不设置初始值，则属性初始值和父Bean相同，否则等于子Bean重新设置的初始值
        System.out.println(derivedTestBean.getName() + ":" + derivedTestBean.getAddress());  // 输出 child:null , 子Bean中设置的新属性，仅仅在子Bean中才可以获取

        // 父Bean中的属性仅仅在子Bean初始化时才对子Bean有约束作用(而且还得看子Bean是否覆写父Bean的属性值)。一旦创建实例后，约束便消失。此时修改父Bean中属性的值并不会影响子Bean的值
        parentTestBean.setName("parent");
        System.out.println(parentTestBean.getName() + ":" + parentTestBean.getAge());  // 输出 parent:20
        System.out.println(derivedTestBean.getName() + ":" + derivedTestBean.getAge());  // 输出 child:20

        // 同上。Bean实例化后，父Bean与子Bean之间的约束便不复存在
        parentTestBean.setAge("15");
        System.out.println(parentTestBean.getName() + ":" + parentTestBean.getAge());  // 输出 parent:15
        System.out.println(derivedTestBean.getName() + ":" + derivedTestBean.getAge());  // 输出 child:20

        // 父Bean永远无法获得子Bean所独有的属性，无论是否实例化，是否重新赋值
        derivedTestBean.setAddress("China");
        // System.out.println(parentTestBean.getName() + ":" + parentTestBean.getAddress());  // 报错
        System.out.println(derivedTestBean.getName() + ":" + derivedTestBean.getAddress());  // 输出 child:China

        return R.ok();
    }

    /**
     * 测试Bean的继承关系
     *
     * @return
     */
    @GetMapping("/test2")
    public R test2() {
        // 重点内容，如果先调用test1，再调用test2，则下方依次输出 parent:15 和 child:20:China
        // 如果没有调用test1就调用test2，则下方依次输出 null:20 和 child:20:null
        // 这正是之前说的，spring创建Bean默认是以 单例 模式创建，即一旦Bean创建后，在任何地方使用该Bean都是同一个Bean，任何时刻对Bean所做的修改都会被保存
        // 因此并不建议对 entity 类对象创建Bean，就像项目中所有使用到Entity的地方都是用 new Entity 来用
        // 创建成Bean的往往都是服务层/Dao层/内部持久化配置，因为这些内容只会被调用，而不会被修改
        System.out.println(parentTestBean.getName() + ":" + parentTestBean.getAge());
        System.out.println(derivedTestBean.getName() + ":" + derivedTestBean.getAge() + ":" + derivedTestBean.getAddress());
        return R.ok();
    }

    /**
     * 测试Bean的扩展点功能
     *
     * @return
     */
    @Autowired
    PropertiesTestBean propertiesTestBean;

    @GetMapping("/test3")
    public R test3() {
        System.out.println(propertiesTestBean.getName() + ":" + propertiesTestBean.getAddress());
        return R.ok();
    }

    /**
     * 每次都是用 new AnnotationConfigApplicationContext 测试 FactoryBean
     * 此时单例模式只在这个方法内有效，方法外失效
     *
     * @return
     */
    @GetMapping("/test4")
    public R test4() {
        // 并非使用 @Autowired 自动注入 Factory，而是每次都获取一个新的 AnnotationConfigApplicationContext
        AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext(
                FactoryBeanOneTest.class,
                FactoryBeanSecTest.class);

        // 注册到ApplicationContext中后，使用 ApplicationContext.getBean(驼峰命名法) 进行获取
        // spring自动实现:如果想要获取的Bean就是FactoryBean，那么自动执行其getObject()方法，返回创建后的实例Bean

        // 调用两次 FactoryBeanOneTest
        // 由于 FactoryBeanOneTest 设置了单例模式，所以两次返回的两个 NonBeanTest实例是相同的实例
        NonBeanTest nonBeanOne = (NonBeanTest) applicationContext.getBean("factoryBeanOneTest");
        System.out.println("调用1号Factory-factoryBeanOneTest" + ":" + nonBeanOne.getName() + ":" + nonBeanOne);
        NonBeanTest nonBeanOneR = (NonBeanTest) applicationContext.getBean("factoryBeanOneTest");
        System.out.println("再次调用1号Factory-factoryBeanOneTest" + ":" + nonBeanOneR.getName() + ":" + nonBeanOneR);
        System.out.println("判断两个Bean是否相等:" + nonBeanOne.equals(nonBeanOneR));

        // 调用两次 FactoryBeanSecTest
        // 由于 FactoryBeanSecTest 设置了多例共享模式，所以两次返回的两个 NonBeanTest实例是不同的实例
        NonBeanTest nonBeanSec = (NonBeanTest) applicationContext.getBean("factoryBeanSecTest");
        System.out.println("调用2号Factory-factoryBeanSecTest" + ":" + nonBeanSec.getName() + ":" + nonBeanSec);
        NonBeanTest nonBeanSecR = (NonBeanTest) applicationContext.getBean("factoryBeanSecTest");
        System.out.println("再次调用2号Factory-factoryBeanSecTest" + ":" + nonBeanSecR.getName() + ":" + nonBeanSecR);
        System.out.println("判断两个Bean是否相等:" + nonBeanSec.equals(nonBeanSecR));

        return R.ok();
    }


    /**
     * 但是上面这种使用 new AnnotationConfigApplicationContext 获取Bean有一个严重的问题：
     * 那就是在这个方法内重复获取 Bean 确实是同一个实例(当然只是针对单例模式的而言)
     * 但是一旦退出这个方法，重新进入时，获取的Bean和上次进入方法时获取的一定不是同一个实例(即便单例模式也不是，多例更别提了)
     * 这很好理解，就是因为你每次都 new 了新的 AnnotationConfigApplicationContext
     * 而Bean的单例多例是依赖 AnnotationConfigApplicationContext 实现的(准确的说是AnnotationConfigApplicationContext内的私有变量--单例对象缓存池)
     * new 的 AnnotationConfigApplicationContext都不同了，单例设置肯定直接失效
     *
     * 解决的办法是，永远使用同一个 AnnotationConfigApplicationContext，而 AnnotationConfigApplicationContext 其实就是 ApplicationContext，中文名 应用上下文(或spring容器)
     * 但是spring一旦启动完成后，就不再提供获取 ApplicationContext的接口(如果你在启动完成后打断点就能发现，断点完全找不到 ApplicationContext了)
     * 因此我们无法显式获取当前spring程序的 ApplicationContext，只能隐式获取
     *
     * 幸好，spring提供了这样一个接口让我们在启动后隐式获取已经完成构建的ApplicationContext--ApplicationContextAware 接口。
     * 实现该接口就能在启动后获取容器上下文。
     */
    @Autowired
    private ApplicationContextAwareTest applicationContextAwareTest;

    /**
     * 使用 ApplicationContextAware接口调用FactoryBean
      * @return
     * @throws Exception
     */
    @GetMapping("/test5")
    public R test5() throws Exception {
        NonBeanTest nonBeanOne = (NonBeanTest) applicationContextAwareTest.getBean("factoryBeanOneTest");
        System.out.println("调用1号Factory-factoryBeanOneTest" + ":" + nonBeanOne.getName() + ":" + nonBeanOne);
        NonBeanTest nonBeanOneR = (NonBeanTest) applicationContextAwareTest.getBean("factoryBeanOneTest");
        System.out.println("再次调用1号Factory-factoryBeanOneTest" + ":" + nonBeanOneR.getName() + ":" + nonBeanOneR);
        System.out.println("判断两个Bean是否相等:" + nonBeanOne.equals(nonBeanOneR));

        // 调用两次 FactoryBeanSecTest
        // 由于 FactoryBeanSecTest 设置了多例共享模式，所以两次返回的两个 NonBeanTest实例是不同的实例
        NonBeanTest nonBeanSec = (NonBeanTest) applicationContextAwareTest.getBean("factoryBeanSecTest");
        System.out.println("调用2号Factory-factoryBeanSecTest" + ":" + nonBeanSec.getName() + ":" + nonBeanSec);
        NonBeanTest nonBeanSecR = (NonBeanTest) applicationContextAwareTest.getBean("factoryBeanSecTest");
        System.out.println("再次调用2号Factory-factoryBeanSecTest" + ":" + nonBeanSecR.getName() + ":" + nonBeanSecR);
        System.out.println("判断两个Bean是否相等:" + nonBeanSec.equals(nonBeanSecR));

        return R.ok();
    }

    /**
     * 使用 @Configuration + @Bean 模拟 FactoryBean
     * 是否使用 @Qualifier并不影响，因为本身下面两个Method的名字就是不一样的，所以 @Autowired是可以找到的
     */
    // @Qualifier("nonBeanTestTir")
    @Autowired
    NonBeanTest nonBeanTestTir;

    // @Qualifier("nonBeanTestFor")
    @Autowired
    NonBeanTest nonBeanTestFor;

    @GetMapping("/test6")
    public R test6() {
        // 用 @Bean模拟的Factory
        // 不同的 @Bean必然是不同实例的Bean
        // 但是由于 @Bean注解不能更改单例/多例模式，因此永远都是单例
        System.out.println("使用@Bean模拟的Factory-nonBeanTestTir" + ":" + nonBeanTestTir.getName() + ":" + nonBeanTestTir);
        System.out.println("使用@Bean模拟的Factory-nonBeanTestFor" + ":" + nonBeanTestFor.getName() + ":" + nonBeanTestFor);
        System.out.println("不同@Bean一定是不同实例:" + nonBeanTestTir.equals(nonBeanTestFor));
        return R.ok();
    }

    /**
     * 测试FactoryBean关闭懒加载，在启动阶段就调用 getObject() 方法
     * 由于是单例模式，所以在启动完成后如果重新调用 FactoryBean的getObject方法，不会创建新的Bean实例
     */
    @GetMapping("/test7")
    public R test7() throws Exception {
        NonBeanTest nonBeanFiv = (NonBeanTest) applicationContextAwareTest.getBean("factoryBeanFivTest");
        System.out.println("调用5号Factory-factoryBeanFivTest" + ":" + nonBeanFiv.getName() + ":" + nonBeanFiv);
        return R.ok();
    }


    /**
     * 测试BeanPostProcessor和BeanFactoryPostProcessor
     *
     * 简单理解 `@Autowired private BeanClass beanName` 的逻辑
     * spring永远是根据 BeanClass 去容器中寻找元类(注意是元类，也就是BeanDefinition中定义的元类)为 BeanClass 的 Bean。
     *    如果找出的Bean只有唯一一个，那直接返回，并在当前域内以 beanName 标记。
     *    如果找出的Bean有多个，则需要定位。首先使用 @Qualifier("name") 中指定的name去匹配容器中的beanKey，如果匹配不上，报出多例异常。
     *    如果Bean有多个且没有使用 @Qualifier，则使用@Autowired命令中的 beanName 去匹配容器中的 beanKey，如果匹配不上，报出多例异常。
     *
     * 注意，经过 BeanFactoryPostProcessorTest 的操作后，容器中现在有两个元类为 BeanPostProcessorTestSecBean 的bean，
     *   一个的beanKey为beanPostProcessorTestBean--原始元类为BeanPostProcessorTestBean，但是被B.F.PostProcessor替换了元类信息，
     *   另一个的beanKey为beanPostProcessorTestSecBean，是未经操作的原始SecBean
     *
     * 因此这里如果使用`@Autowired BeanPostProcessorTestBean beanPostProcessorTestBean` 是会报错的，因为此时容器中根本没有元类为 BeanPostProcessorTestBean 的Bean。
     * 而这里自动注入的两个类 都是元类为 BeanPostProcessorTestSecBean 的Bean, 在下面的打印中也能看出来，打印的属性都是SecBean中的
     */
    @Autowired
    private BeanPostProcessorTestSecBean beanPostProcessorTestBean;
    @Autowired
    private BeanPostProcessorTestSecBean beanPostProcessorTestSecBean;
    @GetMapping("/test8")
    public R test8() throws Exception{
        // 这个Bean被 B.PostProcessor 处理过，因此首次拿来时就已经被改过值了
        System.out.println("首次启动完成后Bean的属性值:" + beanPostProcessorTestBean);
        beanPostProcessorTestBean.setName("Handel");
        System.out.println("首次启动完成后修改Bean的属性值:" + beanPostProcessorTestBean);

        // 这个Bean是没有任何操作的，因此首次拿过来时就是初始值
        System.out.println("首次启动完成后Bean的属性值:" + beanPostProcessorTestSecBean);
        beanPostProcessorTestSecBean.setName("SecHandel");
        System.out.println("首次启动完成后修改Bean的属性值:" + beanPostProcessorTestSecBean);

        return R.ok();
    }
}


